package org.luckyjourney.schedul;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.luckyjourney.constant.AuditStatus;
import org.luckyjourney.constant.RedisConstant;
import org.luckyjourney.entity.Setting;
import org.luckyjourney.entity.video.Video;
import org.luckyjourney.entity.vo.HotVideo;
import org.luckyjourney.service.SettingService;
import org.luckyjourney.service.video.VideoService;
import org.luckyjourney.util.RedisCacheUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @description: 热度排行榜
 * @Author: Xhy
 * @CreateTime: 2023-10-31 00:50
 */
@Component
public class HotRank {

    @Autowired
    private VideoService videoService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private SettingService settingService;

    @Autowired
    private RedisCacheUtil redisCacheUtil;

    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper om = new ObjectMapper();

    {
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
    }

    /**
     * 热度排行榜
     */
    @Scheduled(cron = "0 31 * * * ?")
    // @Scheduled(cron = "0 0 */1 * * ?")
    public void hotRank() {
        // 思考：为什么用堆存储videos？
        final TopK topK = new TopK(11, new PriorityQueue<>(10, Comparator.comparing(HotVideo::getHot)));
        long limit = 1000;
        // TODO 多线程优化/fork join 模型
        long id = 0;
        List<Video> videos = videoService.list(new LambdaQueryWrapper<Video>()
                .select(Video::getId, Video::getShareCount, Video::getHistoryCount, Video::getStartCount, Video::getFavoritesCount,
                        Video::getGmtCreated, Video::getTitle)
                .gt(Video::getId, id)
                .eq(Video::getAuditStatus, AuditStatus.SUCCESS)
                .eq(Video::getOpen, 0)
                .last("limit " + limit));
        // 1. 全表扫描，每次拿1000个，实现分页查询
        while (!ObjectUtils.isEmpty(videos)) {
            for (Video video : videos) {
                // 半衰期算法计算热度并封装HotVideo类
                Long shareCount = video.getShareCount();
                Double historyCount = video.getHistoryCount() * 0.8;
                Long startCount = video.getStartCount();
                Double favoritesCount = video.getFavoritesCount() * 1.5;
                final Date date = new Date();
                long t = date.getTime() - video.getGmtCreated().getTime();
                // 随机获取6位数,用于去重 无意义
                // final double v = weightRandom();
                // final double hot = hot(shareCount + historyCount + startCount + favoritesCount + v, TimeUnit.MILLISECONDS.toDays(t));
                final double hot = hot(shareCount + historyCount + startCount + favoritesCount, TimeUnit.MILLISECONDS.toDays(t));
                final HotVideo hotVideo = new HotVideo(hot, video.getId(), video.getTitle());
                topK.add(hotVideo);
            }
            // 要求id有序
            id = videos.get(videos.size() - 1).getId();
            videos = videoService.list(new LambdaQueryWrapper<Video>()
                    .select(Video::getId, Video::getShareCount, Video::getHistoryCount, Video::getStartCount, Video::getFavoritesCount,
                            Video::getGmtCreated, Video::getTitle).gt(Video::getId, id)
                    .gt(Video::getId, id)
                    .eq(Video::getAuditStatus, AuditStatus.SUCCESS)
                    .eq(Video::getOpen, 0)
                    .last("limit " + limit));
        }
        // 2. 存储热门排行榜到Redis中
        final byte[] key = RedisConstant.HOT_RANK.getBytes();
        final List<HotVideo> hotVideos = topK.get();
        // final Double minHot = hotVideos.get(0).getHot();
        Double minHot = hotVideos.get(hotVideos.size() - 1).getHot();
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (HotVideo hotVideo : hotVideos) {
                final Double hot = hotVideo.getHot();
                hotVideo.setHot(null);
                connection.zAdd(key, hot, jackson2JsonRedisSerializer.serialize(hotVideo));
            }
            return null;
        });
        // 会将热度排行榜榜尾删除掉
        redisTemplate.opsForZSet().removeRangeByScore(RedisConstant.HOT_RANK, 0, minHot);
        // 解决旁路缓存缺点：主动将热门视频缓存到redis中
        List<Long> videoIds = hotVideos.stream().map(HotVideo::getVideoId).collect(Collectors.toList());
        firstCache(videoIds);
    }

    /**
     * 主动将热门视频缓存到redis中
     * @param videoIds
     * @return void
     */
    private void firstCache(List<Long> videoIds) {
        // 多缓存了最后一位的视频信息（videoIds.size()=11）
        List<Video> videos = videoService.listByIds(videoIds);
        if(ObjectUtils.isEmpty(videos))
            return;
        videoService.setUserVoAndUrl(videos);
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (Video video : videos) {
                String key = RedisConstant.VIDEO_STOCK + video.getId();
                // 序列化方式与videoService的不一致
                connection.set(key.getBytes(), jackson2JsonRedisSerializer.serialize(video));
                connection.expire(key.getBytes(), RedisConstant.VIDEO_STOCK_TTL * 24 * 60 * 60);
            }
           return null;
        });
    }

    // 热门视频,没有热度排行榜实时且重要
    // @Scheduled(cron = "0 0 */3 * * ?")
    @Scheduled(cron = "0 21 * * * ?")
    public void hotVideo() {
        // 分片查询3天内的视频
        int limit = 1000;
        long id = 1;
        int days = 30;  // 热门时效
        List<Video> videos = videoService.selectNDaysAgeVideo(id, days, limit);
        final Double hotLimit = settingService.list(new LambdaQueryWrapper<>()).get(0).getHotLimit();
        Calendar calendar = Calendar.getInstance();
        int today = calendar.get(Calendar.DATE);
        final ArrayList<Long> hotVideos = new ArrayList<>();
        while (!ObjectUtils.isEmpty(videos)) {
            for (Video video : videos) {
                Long shareCount = video.getShareCount();
                Double historyCount = video.getHistoryCount() * 0.8;
                Long startCount = video.getStartCount();
                Double favoritesCount = video.getFavoritesCount() * 1.5;
                final Date date = new Date();
                long t = date.getTime() - video.getGmtCreated().getTime();
                final double hot = hot(shareCount + historyCount + startCount + favoritesCount, TimeUnit.MILLISECONDS.toDays(t));
                // 大于X热度说明是热门视频
                if (hot > hotLimit) {
                    hotVideos.add(video.getId());
                }
            }
            // 通过主键实现分页
            id = videos.get(videos.size() - 1).getId();
            videos = videoService.selectNDaysAgeVideo(id, days, limit);
        }
        // RedisConstant.HOT_VIDEO + 今日日期 作为key  达到元素过期效果
        if (!ObjectUtils.isEmpty(hotVideos)){
            // 热门视频id放入redis中
            String key = RedisConstant.HOT_VIDEO + today;
            redisTemplate.opsForSet().add(key, hotVideos.toArray(new Object[0]));
            redisTemplate.expire(key, 3, TimeUnit.DAYS);

        }
    }

    static double a = 0.011;

    /**
     * 半衰期算法计算热度
     * @param  * 参数 weight 权重
     * 参数 t 已发布天数
     * @return
    */
    public static double hot(double weight, double t) {
        return weight * Math.exp(-a * t);
    }


    public double weightRandom() {
        int i = (int) ((Math.random() * 9 + 1) * 100000);
        return i / 1000000.0;
    }

}
